package clue.editor;

import java.awt.Color;
import java.awt.TrayIcon.MessageType;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import objects.BoardData;
import objects.Card;
import objects.CardType;
import objects.Character;
import objects.Envelope;
import objects.Player;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import utils.CaseMachine;
import utils.ValidityManager;

public class FileHandler {
	
	/**
	 * Saves the current state of the board to XML.
	 * The state of the board has to be a valid (playable) state
	 * for the save method to work.
	 * @param clueFrame
	 */
	public static boolean saveFile()
	{
		boolean blIsValid = false;
		// Parse board data from frames.
		BoardData brdCollectedData = BoardDataParser.parseFrameToBoardData();
		Collections.sort(brdCollectedData.getPlayers());
		String ErrMSG = ValidityManager.IsValidBoardData(brdCollectedData);
		
		if (ErrMSG.isEmpty())
		{
			// Choose a file. Filter to XMLs.s
			JFileChooser chooser = new JFileChooser();
			FileFilter ft = new FileNameExtensionFilter("Xml files", "xml", "Xml",
					"XML");

			int returnVal = chooser.showSaveDialog(null);

			File file = chooser.getSelectedFile();
			
			// As long as the user didn't choose a proper file,
			// try again.
			while (!ft.accept(file) && returnVal == JFileChooser.APPROVE_OPTION) {
				String errorMessage = "File must be xml";

				JOptionPane.showMessageDialog(ClueFrame.getInstance(), errorMessage, "Error",
						MessageType.ERROR.ordinal());

				returnVal = chooser.showSaveDialog(null);

				file = chooser.getSelectedFile();
			}

			if (returnVal == JFileChooser.CANCEL_OPTION) {
				return false;
			}
			
			// If the user chose a valid XML file, proceed.
			if (outputFileIsValid(file, ClueFrame.getInstance())) 
			{
				Envelope envToSave = brdCollectedData.getEnvelope();
				
				List<objects.Player> arrPlayers = brdCollectedData.getPlayers();
				Document docXml = createDocument(arrPlayers, envToSave);
				if (docXml != null)
				{
					try 
					{
						// write the content into xml file
						TransformerFactory transformerFactory = TransformerFactory.newInstance();
						Transformer transformer = transformerFactory.newTransformer();
						transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
				        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
				        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
				        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
						DOMSource source = new DOMSource(docXml);
						StreamResult result = new StreamResult(file);
						transformer.transform(source, result);
						ClueFrame.getInstance().setMessage("Saved file successfully.", Color.BLUE);
						blIsValid = true;
					} 
					catch (Exception ex) 
					{
						ClueFrame.getInstance().setMessage("Failed to save XML. make sure it is valid." + '\n' +
							     ex.getMessage(), Color.RED);
					}
				}
			}
		}
		else
		{
			ClueFrame.getInstance().setMessage(ErrMSG + '\n', Color.RED);
		}
		
		return blIsValid;
	}

	/**
	 * Load board data from XML.
	 * XML has to be valid for operation to succeed.
	 * @param clueFrame - The screen frames.
	 * @return
	 */
	public static BoardData loadXmlFile()
	{
		BoardData brdRetVal = BoardDataParser.parseFrameToBoardData();
		
		JFileChooser chooser = new JFileChooser();
		
		int returnVal = chooser.showOpenDialog(ClueFrame.getInstance());
		
		if (returnVal == JFileChooser.CANCEL_OPTION) {
			return null;
		}
		
		// If selected file exists, try to load from it.
		if (chooser.getSelectedFile() != null && chooser.getSelectedFile().exists()) 
		{
			brdRetVal = readXml(chooser.getSelectedFile());
			
		}
		
		if (brdRetVal != null)
		{
			String strErrMsg = ValidityManager.IsValidBoardData(brdRetVal);
			if (!strErrMsg.isEmpty())
			{
				ClueFrame.getInstance().setMessage(strErrMsg + '\n', Color.RED);
				brdRetVal = BoardDataParser.parseFrameToBoardData();
			}
		}
		
		return brdRetVal;
		
	}
	
	/**
	 * Checks if the output file exists, and if the user accepts overriding it.
	 * @param outputFile - the file to save to.
	 * @param clueFrame - screen frames.
	 * @return - true if we can save to the file, false otherwise.
	 */
	private static boolean outputFileIsValid(File outputFile,
			ClueFrame clueFrame) {
		boolean fileIsValid = false;

		if (outputFile.exists()) {
			int result = JOptionPane.showConfirmDialog(clueFrame,
					"File exists, overwrite?", "File exists",
					JOptionPane.YES_NO_CANCEL_OPTION);
			switch (result) {
			case JOptionPane.YES_OPTION:
				fileIsValid = true;
				break;
			default:
				fileIsValid = false;
			}
		} else {
			fileIsValid = true;
		}
		return fileIsValid;
	}
	
	/**
	 * Create a DOM document according to board data.
	 * @param lstPlayers
	 * @param envEnvToSave
	 * @return
	 */
	public static Document createDocument(List<Player> lstPlayers, Envelope envEnvToSave)
	{
		DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder docBuilder;
		Document doc = null;
		try 
		{
			docBuilder = docFactory.newDocumentBuilder();
			// root elements
			doc = docBuilder.newDocument();
			Element rootElement = doc.createElement("clue");
			doc.appendChild(rootElement);
			rootElement.setAttribute("xsi:noNamespaceSchemaLocation", "clue.xsd");
			rootElement.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
			
			// Envelope Section
			Element elemMurderRoom = doc.createElement("murder_room");
			elemMurderRoom.appendChild(doc.createTextNode(CaseMachine.getCardVal(envEnvToSave.getRoomCard())));
			rootElement.appendChild(elemMurderRoom);
			Element elemMurderWeapon = doc.createElement("murder_weapon");
			elemMurderWeapon.appendChild(doc.createTextNode(CaseMachine.getCardVal(envEnvToSave.getWeaponCard())));
			rootElement.appendChild(elemMurderWeapon);
			Element elemMurderer = doc.createElement("murderer");
			elemMurderer.appendChild(doc.createTextNode(CaseMachine.getCardVal(envEnvToSave.getCharacterCard())));
			rootElement.appendChild(elemMurderer);
			
			
			// Players Section
			Element elemPlayers = doc.createElement("players");
			rootElement.appendChild(elemPlayers);
			
			for (Player plCurr : lstPlayers)
			{
				// staff elements
				Element elemPlayer = doc.createElement("player");
				elemPlayers.appendChild(elemPlayer);
				String strPlName = plCurr.getName();
				String strPlType =  plCurr.getType();
				
				switch (strPlType.toUpperCase())
				{
					case "AI": strPlType = "COMPUTER";
						break;
					case "HUMAN": strPlType = "HUMAN";
						break;
				}
				
				String strPlChar = plCurr.getCharacter().name();
				elemPlayer.setAttribute("type", strPlType);
				elemPlayer.setAttribute("name", strPlName);
				elemPlayer.setAttribute("character", CaseMachine.getCharFromCard(Character.valueOf(strPlChar)));
				List<Card> arrCards = plCurr.getCards();
				Element elemCards = doc.createElement("cards");
				elemPlayer.appendChild(elemCards);
				Collections.reverse(arrCards);
				for (Card crdCurr : arrCards)
				{
					// staff elements
					Element elemCard = doc.createElement("card");
					elemCards.appendChild(elemCard);
					String strCrType =  crdCurr.getType().name();
					switch (strCrType.toLowerCase())
					{
						case "character": strCrType = "suspect";
							break;
					}
					elemCard.setAttribute("type", strCrType.toLowerCase());
					elemCard.setAttribute(strCrType.toLowerCase(), CaseMachine.getCardVal(crdCurr));
				}
			}
	 	} 
		catch (ParserConfigurationException ex) 
		{
			ClueFrame.getInstance().setMessage("Failed to parse XML. Make sure it is valid. Message: " + '\n' +
				     ex.getMessage(), Color.RED);
			doc = null;
		}
		catch (Exception ex) 
		{
			ClueFrame.getInstance().setMessage("General Error. Make sure the board data is valid. Message: " + '\n' +
				     ex.getMessage(), Color.RED);
			doc = null;
		}
		
		return doc;		
	}
	   
    /**
     * @return true if xml read was successful and false if not
     * @throws IOException 
     * @throws SAXException 
     * @throws ParserConfigurationException 
     */
    private static BoardData readXml(File xmlFile)
    {
    	List<Player> players = null;
    	Envelope env = null;
    	BoardData bdRetVal = null;
    	Document gameInfo = null;

    	try
    	{
    		gameInfo = parseXMLFromFile(xmlFile);
    		
    		if (gameInfo != null)
    		{
    			ClueFrame.getInstance().setMessage("Loaded XML file." + '\n', Color.BLUE);
    			
    			//load each player and player's cards.
    			NodeList nodes = (NodeList) getNodesetFromDocument(gameInfo,"//players/player");
    			if (nodes.getLength() == 0)
    			{
    				ClueFrame.getInstance().setMessage("XML File has no players." + '\n', Color.RED);
    			}
    			else
    			{
                    // Iterate on all nodes and create players.
    				players = new ArrayList<Player>();
	    			for (int i = 0; i < nodes.getLength(); i++) 
	    			{
	    				// Get a player's attributes
	    				// NOT ALL WEAPON/CHAR/ROOM cards associated with players!
	    				String character = nodes.item(i).getAttributes().getNamedItem("character").getNodeValue();
	    				String name = nodes.item(i).getAttributes().getNamedItem("name").getNodeValue();
	    				String type = nodes.item(i).getAttributes().getNamedItem("type").getNodeValue();
	    				
	    				Character playerCharacter = CaseMachine.getCharFromString(character);
	    				Player plPlayerToAdd = new Player();
	    				plPlayerToAdd.setCharacter(playerCharacter);
	    				plPlayerToAdd.setName(name);
	    				plPlayerToAdd.setOrder(i+1);
	    				plPlayerToAdd.setType(type.toUpperCase());
	    				
	    				// Iterate on the cards of the current player.
	    				NodeList cardNodes = (NodeList) getNodesetFromDocument(nodes.item(i),"cards/card");
	    				Integer t = cardNodes.getLength();
	    				List<Card> lstCards = new ArrayList<Card>();
	    				for (int j = 0; j < t; j++) 
		    			{
	    					String cardType = cardNodes.item(j).getAttributes().getNamedItem("type").getNodeValue();
	    					String cardValue = cardNodes.item(j).getAttributes().getNamedItem(cardType).getNodeValue();
	    					// After getting a card data, we need to parse its text
	    					// to a proper card.
	    					Card cardToAdd = null;		
	    					try
    						{
		    					switch (cardType) 
	    				        {
	    				            case "weapon":
	    				            	{
	    				            		cardToAdd = new Card(CaseMachine.getCardTypeFromString(cardType), CaseMachine.getWeaponFromString(cardValue).name());	
	    				            		break;
	    				            	}
	    				            case "room":
	    				            	{
	    				            		cardToAdd = new Card(CaseMachine.getCardTypeFromString(cardType), CaseMachine.getRoomFromString(cardValue).name());
	    				            		break;
	    				            	}
	    				            case "suspect":
	    				            	{
	    				            		cardToAdd = new Card(CaseMachine.getCardTypeFromString(cardType), CaseMachine.getCharFromString(cardValue).name());
	    				            		break;
	    				            	}
	    				        }
    						 }
    						catch (Exception ex)
    						{
    							ClueFrame.getInstance().setMessage("Malformed card found. Check XML is valid." + '\n' +
    								     "Player " + plPlayerToAdd.getName() + ", Card: " + cardType + " " + cardValue, Color.RED);
    							return null;
    						}
    				       
	    					
	    					if (cardToAdd != null)
	    					{
	    						lstCards.add(cardToAdd);
	    					}
		    			}
	    				plPlayerToAdd.setCards(lstCards);
	    				players.add(plPlayerToAdd);

	    			}
    			}
    		}
    		
    	}
    	catch(Exception ex)
    	{
    		ClueFrame.getInstance().setMessage("Failed to parse XML. Make sure it is valid.", Color.RED);
    		return null;
    	}
    	
    	
		env = new Envelope();
		String strRoomName = (String) getStringFromXpath(gameInfo,
				"//murder_room/text()");
		env.setRoomCard(new Card(CardType.ROOM, CaseMachine.getRoomFromString(
				strRoomName).name()));

		String strWeaponName = (String) getStringFromXpath(gameInfo,
				"//murder_weapon/text()");
		env.setWeaponCard(new Card(CardType.WEAPON, CaseMachine
				.getWeaponFromString(strWeaponName).name()));

		String strCharName = (String) getStringFromXpath(gameInfo,
				"//murderer/text()");
		Character chr = CaseMachine.getCharFromString(strCharName);
		env.setCharacterCard(new Card(CardType.CHARACTER, chr.name()));

		if (bdRetVal == null && players != null && env != null) {
			bdRetVal = new BoardData();
			bdRetVal.setPlayers(players);
			bdRetVal.setEnvelope(env);
		}
    	
    	
		return bdRetVal;
    }
    
    /**
     * Take a file and convert it to Document object.
     * @param xmlFile
     * @return a document
     */
    public static Document parseXMLFromFile(File xmlFile)
    {
    	Document doc = null;
    	try
    	{
    		 DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
    		 docBuilderFactory.setNamespaceAware(true);
             DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
             doc = docBuilder.parse(xmlFile);
    	}
    	catch(SAXException ex)
    	{
    		ClueFrame.getInstance().setMessage("Failed to parse XML. Make sure it is valid." + '\n' +
    									     ex.getMessage(), Color.RED);
    	}
    	catch(IOException ex)
    	{
    		ClueFrame.getInstance().setMessage("Failed to read. Make sure it exists." + '\n' +
				     ex.getMessage(), Color.RED);

    	} 
    	catch (ParserConfigurationException ex)
    	{
    		ClueFrame.getInstance().setMessage("Failed to parse XML. Make sure it is valid." + '\n' +
				     ex.getMessage(), Color.RED);
    	}

		return doc;
    }
    
    /**
     * Get a node set from the entire doc.
     * @param doc
     * @param xPath
     * @return
     */
    public static Object getNodesetFromDocument(Document doc, String xPath)
    {
    	Object result = null;
    	XPathFactory factory = XPathFactory.newInstance();
    	XPath xpath = factory.newXPath();
    	XPathExpression expr;
		try 
		{
			expr = xpath.compile(xPath);
			result = expr.evaluate(doc, XPathConstants.NODESET);
		}
		catch (XPathExpressionException ex) 
		{
			ClueFrame.getInstance().setMessage("Failed getting node "+ xPath + " From XML." + '\n' +
				     ex.getMessage(), Color.RED);
		}
		
		return result;
    }

    /**
     * Get a node set from a given node.
     * @param node - a father node to seach for son nodes in.
     * @param xPath - the path to the son node. 
     * @return - a nodeset that is a son of @node
     */
    public static Object getNodesetFromDocument(Node node, String xPath)
    {
    	Object result = null;
    	XPathFactory factory = XPathFactory.newInstance();
    	XPath xpath = factory.newXPath();
    	XPathExpression expr;
		try 
		{
			expr = xpath.compile(xPath);
			result = expr.evaluate(node, XPathConstants.NODESET);
		}
		catch (XPathExpressionException ex)
		{
			ClueFrame.getInstance().setMessage("Failed getting node "+ xPath + " From Node." + '\n' +
			     ex.getMessage(), Color.RED);
		}
		return result;
    }
    
    /**
     * Get a string value from an XPATH in the xml.
     * @param doc
     * @param xPath
     * @return
     */
    public static Object getStringFromXpath(Document doc, String xPath)
    {
    	Object result = null;
    	XPathFactory factory = XPathFactory.newInstance();
    	XPath xpath = factory.newXPath();
    	XPathExpression expr;
		try 
		{
			expr = xpath.compile(xPath);
			result = expr.evaluate(doc, XPathConstants.STRING);
		}
		catch (XPathExpressionException ex)
		{
			ClueFrame.getInstance().setMessage("Failed getting node "+ xPath + " From XML." + '\n' +
				     ex.getMessage(), Color.RED);
}
		return result;
    }
}
